using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using CustomCMS.API.Wrapper;
using System.IO;
using log4net;
using CustomCMS.API.ObjectModel;
using System.Globalization;
using System.Threading;

namespace CustomCMS.API.Persistance
{
    public class FileSystemPersistanceProvider : BasePersistanceProvider, IPersistanceProvider
    {
        internal const string FILEINFO_EXTENSTION = ".fileinfo";
        internal const string GENERATE_LOCK = "generate_lock.xml";

		private static readonly ILog _log = LogManager.GetLogger(typeof(FileSystemPersistanceProvider));

		#region IPersistanceProvider Members

        public void SetTemplate(string templateId, string directoryId, string userId, string content, string comment, string title)
        {
			//removing first \ and replacing * with \
			string filename = GetElementPath(directoryId, templateId, ElementType.template);

			int revision = 1;

			if (File.Exists(filename))
			{
				ElementInfo info = FileSystemWrapper.GetFileSummary(filename, RepositoryLocation);
				
				if (info.LockedBy.Length > 0 && string.Compare(userId, info.LockedBy, true) != 0)
					throw new Exception("Template is locked by: " + info.LockedBy);

				revision = ArchiveFile(filename) + 1;
			}
			Wrapper.FileSystemWrapper.SetFileContent(filename, content);
			Wrapper.FileSystemWrapper.SetFileSummary(filename, new ElementInfo(userId, title, DateTime.Now, revision, comment, string.Empty, string.Empty, string.Empty));
		}

        public string GetTemplatesTree()
        {
			string path = GetElementPath("", "", ElementType.template);

			StringBuilder str = new StringBuilder();

			str.Append("<node type=\"dir\" id=\"root*template\">");

			MapChildren(str, path,"root*template");

			str.Append("</node>");

			return str.ToString();
		}

        public CustomCMS.API.ObjectModel.Template GetTemplate(string templateId,string directoryId)
        {
			Template template = new Template();

			string filename = GetElementPath(directoryId, templateId, ElementType.template);
            template.FileName = filename;

			template.Content = FileSystemWrapper.GetFileContent(filename);

			ElementInfo info = FileSystemWrapper.GetFileSummary(filename, RepositoryLocation);

            if (info != null)
            {
                template.Comment = info.Comment;
                template.Location = directoryId;
                template.Locked = info.LockedBy.Length > 0;
                template.LockingUser = info.LockedBy;
                template.Title = info.Title;
            }

            return template;
		}

		public void CreateDirectory(ElementType type, string directoryId)
        {
			directoryId = Regex.Replace(directoryId.Replace("*", "\\"), @"^\\", "");
			string path = Path.Combine(Path.Combine(RepositoryLocation, "root"), type.ToString());
			path = Path.Combine(path, directoryId);

			if (Directory.Exists(path))
				throw new Exception("Directory already exist");

			FileSystemWrapper.CreateDirectory(path);
        }
        
        public string GetUsingPages(string templateId,string directoryId)
        {
            throw new Exception("The method or operation is not implemented.");
        }
        
        public string GetPagesTree()
        {
			string path = GetElementPath("", "", ElementType.page);

			StringBuilder str = new StringBuilder();

			str.Append("<node type=\"dir\" id=\"root*page\">");

			MapChildren(str, path, "root*page");

			str.Append("</node>");

			return str.ToString();
        }

        public CustomCMS.API.ObjectModel.Page GetPage(string pageId, string directoryId, out List<string> missingTemplates)
        {
			string filename = GetElementPath(directoryId, pageId, ElementType.page);
			ElementInfo info = FileSystemWrapper.GetFileSummary(filename, RepositoryLocation);

            Dictionary<string, int> states = new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase);
            XmlDocument xml = new XmlDocument();
            xml.LoadXml(info.SectionStates == "" || info.SectionStates == null? "<states/>" : info.SectionStates);
            foreach (XmlNode n in xml.SelectNodes("/states/section")) states[n.Attributes["name"].Value] = int.Parse(n.Attributes["state"].Value);

            Page page = new Page();
            page.FileName = filename;
            page.Template = info.Template;
            page.Content = GetPageContent(info.Template, filename, states, out missingTemplates);
			page.Comment = info.Comment;
            page.Location = directoryId;
			page.Locked = info.LockedBy.Length > 0;
			page.LockingUser = info.LockedBy;
			page.Title = info.Title;

			return page;
		}

        public string GetVersions(string id, string directoryId, ElementType type)
        {
            return GetFileVersions(GetElementPath(directoryId, id, type));
        }

        private string GetFileVersions(string filename)
        {
            string newest = Regex.Replace(filename, "_history[/\\\\]|[/\\\\][^/\\\\]+(?:\\.\\d+){1,2}$", "");
            string historyDir = GetHistoryDirectory(newest);
            string[] files = Directory.Exists(historyDir)? Directory.GetFiles(historyDir) : new string[0];

            List<ElementInfo> versions = new List<ElementInfo>();
            foreach (string file in files)
            {
                if (Regex.IsMatch(file, "\\"+FILEINFO_EXTENSTION+"(?:\\.\\d+(?:\\.\\d+)?)?$")) continue;
                try
                {
                    ElementInfo info = FileSystemWrapper.GetFileSummary(file, RepositoryLocation);
                    if (info != null) versions.Add(info);
                }
                catch (Exception) { }
            }
            versions.Add(FileSystemWrapper.GetFileSummary(newest, RepositoryLocation));
            versions.Sort(ElementInfo.LaterThan);

            string current = FileSystemWrapper.GetFileSummary(filename, RepositoryLocation).Id;

            return "<versions current="+ElementInfo.Quote(current)+">" + String.Join("", versions.ConvertAll<string>(ElementInfo.ToXml).ToArray()) + "</versions>";
        }

        public void SetPage(string pageId, string directoryId, string userId, string title, string comment, string content, string templateId, string sectionStates)
        {
            if (!File.Exists(GetElementPath(templateId, "", ElementType.template)))
                throw new Exception("master template does not exist");

            string filename = GetElementPath(directoryId, pageId, ElementType.page);

            int revision = 1;

            if (File.Exists(filename))
            {
                ElementInfo info = FileSystemWrapper.GetFileSummary(filename, RepositoryLocation);

                if (info.LockedBy.Length > 0 && string.Compare(userId, info.LockedBy, true) != 0)
                    throw new Exception("Page is locked by: " + info.LockedBy);

                revision = ArchiveFile(filename) + 1;
            }

            Wrapper.FileSystemWrapper.SetFileContent(filename, content);
            Wrapper.FileSystemWrapper.SetFileSummary(filename, new ElementInfo(userId, title, DateTime.Now, revision, comment, templateId, string.Empty, sectionStates));
        }

        public void SetSectionStates(string userId, string pageId, string directoryId, string states)
        {
            string filename = GetElementPath(directoryId, pageId, ElementType.page);
            ElementInfo info = FileSystemWrapper.GetFileSummary(filename, RepositoryLocation);
            if (info.LockedBy.Length > 0 && string.Compare(userId, info.LockedBy, true) != 0) throw new Exception("Page is locked by: " + info.LockedBy);
            Wrapper.FileSystemWrapper.SetFileSummary(filename, new ElementInfo(info.Author, info.Title, info.Date, info.Revision, info.Comment, info.Template, info.LockedBy, states));
        }

        private string EscapeRegex(string s)
        {
            return s == null? s : Regex.Replace(s, "([*+{}\\[\\]\\\\?()^$\\.])", "\\$1");
        }

        public void MoveElement(ElementType type, string elementId, string directoryId, string to, string userId)
        {
            string org = GetElementPath(directoryId, elementId, type);
            string dest = GetElementPath(directoryId, to, type);
            if (type == ElementType.dir)
            {
                throw new Exception("Moving and renaming directories is not supported yet.");
                if (!Directory.Exists(org)) throw new Exception("Origin directory '" + org + "' does not exist");
                if (Directory.Exists(dest)) throw new Exception("Destination directory '" + dest + "' already exists");
            }
            else
            {
                if (!File.Exists(org)) throw new Exception("Origin file '" + org + "' does not exist");
                if (File.Exists(dest)) throw new Exception("Destination file '" + dest + "' already exists");
            }

            if (type == ElementType.page || type == ElementType.template)
            {
                ElementInfo info = FileSystemWrapper.GetFileSummary(org, RepositoryLocation);
                if (info != null && info.LockedBy != null && info.LockedBy != "" && info.LockedBy != userId) throw new Exception("Element is locked by '" + info.LockedBy + "'");

                List<string> locked = new List<string>();
                if (type == ElementType.template)
                {
                    string d = "([/\\\\\\*]?)" + (directoryId == null ? "" : EscapeRegex(Regex.Replace(directoryId, "^[/\\\\\\*]", ""))) + EscapeRegex(elementId);
                    Regex dir = new Regex("^" + d + "\\s*$", RegexOptions.IgnoreCase);
                    Regex o = new Regex("(<cms:include\\s+path=(\"|'))" + d + "\\2", RegexOptions.IgnoreCase);
                    List<string> list = GetFlatList();
                    List<string> dependants = new List<string>();
                    foreach (string item in list)
                    {
                        string filename = Path.Combine(RepositoryLocation, item.Replace('*', '/'));
                        info = FileSystemWrapper.GetFileSummary(filename, RepositoryLocation);
                        if (dir.IsMatch(info.Template) || o.IsMatch(FileSystemWrapper.GetFileContent(filename)))
                        {
                            dependants.Add(item);
                            if (info != null && info.LockedBy != null && info.LockedBy != "" && info.LockedBy != userId) { locked.Add(item.Replace('*', '/')); if (locked.Count == 8) break; }
                        }
                    }
                    if (locked.Count != 0) throw new Exception("Could not move template, because the following items use it and are currently locked:\n\n"+string.Join("\n",locked.ToArray()));

                    d = "$1$3" + Regex.Replace(Regex.Replace(directoryId, "^[/\\\\\\*]", "") + to, "\\$", "$$") + "$2";
                    foreach (string item in dependants)
                    {
                        string filename = Path.Combine(RepositoryLocation, item.Replace('*', '/'));
                        info = FileSystemWrapper.GetFileSummary(filename, RepositoryLocation);
                        if (info != null && info.LockedBy != null && info.LockedBy != "" && info.LockedBy != userId) { locked.Add(item.Replace('*', '/')); continue; }
                        string text = FileSystemWrapper.GetFileContent(filename);
                        if (dir.IsMatch(info.Template)) info.Template = dir.Match(info.Template).Groups[1].Value + to;
                        else if (!o.IsMatch(text)) continue;
                        int revision = ArchiveFile(filename) + 1;
                        Wrapper.FileSystemWrapper.SetFileContent(filename, o.Replace(text, d));
                        Wrapper.FileSystemWrapper.SetFileSummary(filename, new ElementInfo(userId, info.Title, DateTime.Now, revision, info.Comment, info.Template, string.Empty, info.SectionStates));
                    }
                }

                string content = FileSystemWrapper.GetFileContent(org);
                Wrapper.FileSystemWrapper.SetFileContent(dest, content);
                Wrapper.FileSystemWrapper.SetFileSummary(dest, new ElementInfo(userId, info.Title, DateTime.Now, info.Revision + 1, info.Comment, info.Template, info.LockedBy, info.SectionStates));
                Delete(type, org, userId);

                if (locked.Count != 0) throw new Exception("Moved template successfully, but could not update the following dependant items because they were locked:\n\n"+string.Join("\n", locked.ToArray()));
            }
            else if (type == ElementType.image)
            {
                ArchiveFile(org, true);
                File.Move(org, dest);
            }
        }

        private struct FileStructure
		{
			public string FileName;
			public bool IsFile;

			public FileStructure(string filename, bool isFile)
			{
				FileName = filename;
				IsFile = isFile;
			}
		}

		public void DeleteDirectory(ElementType type, string directoryId, string userId)
		{
			if (directoryId == "*")
				throw new Exception("Cannot delete root element");

			string dirName = GetElementPath(directoryId, "", type);

			if (!Directory.Exists(dirName))
				throw new Exception("Directory doesn't exist");

			Stack<FileStructure> subs = new Stack<FileStructure>();

			subs.Push(new FileStructure(dirName, false));

			GetDirectoryTree(type, dirName, subs, true);

            if (type == ElementType.template && subs.Count > 1)
                throw new Exception("To delete a template directory please delete all of its children first.");

			do
			{
				FileStructure fileStruct = subs.Pop();

				if (fileStruct.IsFile)
					Delete(type, Path.GetFileName(fileStruct.FileName), Path.GetDirectoryName(fileStruct.FileName), userId);
				else
					Directory.Delete(fileStruct.FileName,true);
			}
			while (subs.Count > 0);
		}

		private void GetDirectoryTree(ElementType type, string parentDir, Stack<FileStructure> subs, bool throwIfLocked)
		{
			string[] files = Directory.GetFiles(parentDir);

			foreach (string file in files)
			{
				string filename = Path.Combine(parentDir, file);

                if (Path.GetExtension(filename) == FILEINFO_EXTENSTION)
                    continue;

				ElementInfo info = FileSystemWrapper.GetFileSummary(filename, RepositoryLocation);

				if (info.LockedBy.Length > 0 && throwIfLocked)
					throw new Exception(file + " is locked by " + info.LockedBy);

				subs.Push(new FileStructure(filename,true));
			}

			string[] dirs = Directory.GetDirectories(parentDir);

			foreach (string dir in dirs)
			{
				if (dir.ToLower() == ".svn" || dir.ToLower() == "_svn")
					continue;

				subs.Push(new FileStructure(Path.Combine(parentDir, dir),false));

				GetDirectoryTree(type,Path.Combine(parentDir, dir), subs, throwIfLocked);
			}
		}

		private void Delete(ElementType type, string filename, string userId)
		{
			if (!File.Exists(filename)) throw new Exception(type.ToString() + " does not exist");
            string infoname = filename + FILEINFO_EXTENSTION;

			ElementInfo info = FileSystemWrapper.GetFileSummary(filename, RepositoryLocation);
			if (info != null && info.LockedBy.Length > 0 && string.Compare(userId, info.LockedBy, true) != 0)
				throw new Exception("Item is locked by: " + info.LockedBy);

			ArchiveFile(filename);

			File.Delete(filename);
            if (File.Exists(infoname)) File.Delete(infoname);
		}

		public void Delete(ElementType type, string elementId, string directoryId,string userId)
        {
            if (type == ElementType.template)
            {
                _log.Debug("check if the template is not being used by any page");
                List<string> dependants = GetDependants(directoryId+elementId, 8);
                if (dependants.Count > 0) throw new Exception("Template '" + elementId + "' could not be deleted because it is being used by the following items:\n\t" +
                    string.Join("\n\t", dependants.ToArray()));
            }

            string filename = GetElementPath(directoryId, elementId, type);

			Delete(type, filename, userId);
		}

        private List<string> GetFlatList()
        {
            List<string> ids = new List<string>();

            XmlDocument xml = new XmlDocument();
            xml.LoadXml(GetTemplatesTree());
            XmlNodeList list = xml.SelectNodes("//node[@type='file']");
            foreach (XmlNode node in list) ids.Add(node.ParentNode.Attributes["id"].Value + "/" + node.Attributes["id"].Value);

            xml.LoadXml(GetPagesTree());
            list = xml.SelectNodes("//node[@type='file']");
            foreach (XmlNode node in list) ids.Add(node.ParentNode.Attributes["id"].Value + "/" + node.Attributes["id"].Value);

            return ids;
        }

        private List<string> GetDependants(string templateId, int max)
        {
            List<string> ids = new List<string>();

            string templates = GetTemplatesTree();
            XmlDocument xml = new XmlDocument();
            xml.LoadXml(templates);
            ids.AddRange(GetDependants(templateId, xml.SelectSingleNode("/*"), max));
            if ((max -= ids.Count) <= 0) return ids;

            string pages = GetPagesTree();
            xml.LoadXml(pages);
            ids.AddRange(GetDependants(templateId, xml.SelectSingleNode("/*"), max));

            return ids;
        }

        private List<string> GetDependants(string templateId, XmlNode node, int max)
        {
            List<string> ids = new List<string>();
 
            string dirId = node.Attributes["id"].Value;
            Regex re = new Regex("^root[\\*\\/](?:(template)|(page))\\b[\\*\\/]?");
            Match m = re.Match(dirId);
            Boolean isTemplate = m.Success && m.Groups[1].Success;
            Boolean isPage = m.Success && m.Groups[2].Success;
            string path = re.Replace(dirId, "");

            XmlNodeList children = node.ChildNodes;
            for (int i = 0; i != children.Count; ++i)
            {
                string type = children[i].Attributes["type"].Value;
                string id = children[i].Attributes["id"].Value;
                if (type == "file")
                {
                    List<string> missing;
                    if (isPage && IsPageDependant(templateId, GetPage(id, path, out missing)) ||
                        isTemplate && IsTemplateDependant(templateId, GetTemplate(id, path)))
                    {
                        ids.Add(Regex.Replace(dirId + "*" + id, "\\*", "/"));
                        if (--max <= 0)
                        {
                            ids.Add("... and possibly more");
                            return ids;
                        }
                    }
                }
                else if (type == "dir")
                {
                    int n = ids.Count;
                    ids.AddRange(GetDependants(templateId, children[i], max));
                    if ((max -= n) <= 0) return ids;
                }
            }
            return ids;
        }

        private Boolean IsPageDependant(string templateId, CustomCMS.API.ObjectModel.Page page)
        {
            if (IsTemplateEqual(page.Template, templateId)) return true;
            StringBuilder template = new StringBuilder(GetTemplate(page.Template, "*").Content);
            Dictionary<string, string> content = GetContentValues(page.FileName);
            Dictionary<string, string[]> templateVars = GetTemplateKeys(template.ToString(), "variable", "values", "");
            return IsContentDependant(templateId, template, templateVars, content);
        }

        private Boolean IsTemplateDependant(string templateId, CustomCMS.API.ObjectModel.Template template)
        {
            StringBuilder text = new StringBuilder(template.Content);
            Dictionary<string, string> content = new Dictionary<string, string>();// GetContentValues(template.FileName);
            Dictionary<string, string[]> templateVars = GetTemplateKeys(text.ToString(), "variable", "values", "");
            return IsContentDependant(templateId, text, templateVars, content);
        }

        private Boolean IsContentDependant(string templateId, StringBuilder template, Dictionary<string, string[]> templateVars, Dictionary<string, string> content)
        {
            ReplaceVariables(template, templateVars, content);

            MatchCollection matches = Regex.Matches(template.ToString(), "<cms:include\\s+[^>]*?\\bpath\\s*=\\s*\"([^\"]+)\"[^>]*?(?:\\/>|>.*?<\\/cms:include>)", RegexOptions.IgnoreCase);
            foreach (Match match in matches)
            {
                StringBuilder include = new StringBuilder(match.Groups[1].Value);
                ReplaceVariables(include, templateVars, content);
                string includeId = include.ToString();
                if (IsTemplateEqual(templateId, includeId)) return true;

                ObjectModel.Template subTemplate;
                try { subTemplate = this.GetTemplate(include.ToString(), ""); } catch (Exception) { continue; }
                StringBuilder subTemplateContent = new StringBuilder(subTemplate.Content);
                if (IsContentDependant(templateId, subTemplateContent, templateVars, content)) return true;
            }
            return false;
        }

        private static bool IsTemplateEqual(string templateId, string includeId)
        {
            Regex re = new Regex("^[\\*\\/]|^root[\\*\\/]template[\\*\\/]", RegexOptions.IgnoreCase);
            return re.Replace(includeId, "").Equals(re.Replace(templateId, ""), StringComparison.InvariantCultureIgnoreCase);
        }

        public void Lock(ElementType type, string elementId, string directoryId, string userId)
        {
			string filename = GetElementPath(directoryId, elementId, type);

			if (!File.Exists(filename))
				throw new Exception("filename does not exist");

			ElementInfo info = FileSystemWrapper.GetFileSummary(filename, RepositoryLocation);

			if (info.LockedBy.Length > 0 && info.LockedBy.ToLower() != userId)
				throw new Exception("file already locked by " + info.LockedBy);

			info.LockedBy = userId;

			FileSystemWrapper.SetFileSummary(filename, info);
		}

		public void Unlock(ElementType type, string elementId, string directoryId,string userId)
        {
			string filename = GetElementPath(directoryId, elementId, type);

			if (!File.Exists(filename))
				throw new Exception("filename does not exist");

			ElementInfo info = FileSystemWrapper.GetFileSummary(filename, RepositoryLocation);

			if (info.LockedBy.Length > 0 && info.LockedBy.ToLower() != userId)
				throw new Exception("file already locked by " + info.LockedBy);

			info.LockedBy = string.Empty;

			FileSystemWrapper.SetFileSummary(filename, info);
		}

		public CustomCMS.API.ObjectModel.ElementInfo[] GetHistory(ElementType type, string elementId, string directoryId)
        {
            throw new Exception("The method or operation is not implemented.");
        }

		public string GeneratePage(string pageId, string directoryId)
        {
			string filename = GetElementPath(directoryId, pageId, ElementType.page);

			if (!File.Exists(filename)) return filename + " file does not exist.";

			ElementInfo info = FileSystemWrapper.GetFileSummary(filename, RepositoryLocation);

			//image treatment = just copy - no templates...
			if (info == null)
			{
				string resultFilename = GetElementPath(directoryId, pageId, ElementType.www);
				string wwwFolder = resultFilename.Remove(resultFilename.LastIndexOf(Path.GetFileName(resultFilename)));

				if (!Directory.Exists(wwwFolder))
					Directory.CreateDirectory(wwwFolder);

				File.Copy(filename, resultFilename,true);
                return null;
			}
			else //page treatment
			{
                StringBuilder template;
                try
                {
                    template = new StringBuilder(GetTemplate(info.Template, "").Content);
                }
                catch (Exception)
                {
                    return filename + " is missing template:\n\t" + info.Template;
                }

                string warnings;
				string result = ProcessContent(template, filename, out warnings);

				string resultFilename = GetElementPath(directoryId, pageId, ElementType.www);
				string wwwFolder = resultFilename.Remove(resultFilename.LastIndexOf(Path.GetFileName(resultFilename)));

				if (!Directory.Exists(wwwFolder))
					Directory.CreateDirectory(wwwFolder);

				FileSystemWrapper.SetFileContent(resultFilename, result);
                return warnings;
			}
        }

        public void CreateRepository()
        {
            string path = Path.Combine(this.RepositoryLocation, "root");

            if (Directory.Exists(path))
                throw new Exception("Repository already exist in current location");

            FileSystemWrapper.CreateDirectory(path);

            string[] subFolders = new string[] { "template", "page", "_history", "www" };

            foreach(string folder in subFolders)
                FileSystemWrapper.CreateDirectory(Path.Combine(path,folder));
        }

		public void SetImage(System.IO.Stream file, string userId, string filename, string directoryId)
		{
			//check if exist already, and archive it.
			string path = GetElementPath(directoryId, "", ElementType.page);
			
			//save the file
			path = Path.Combine(path, filename);

			FileStream output = File.Create(path);

			BinaryReader br = new BinaryReader(file);
			output.Write(br.ReadBytes((int)file.Length),0,(int)file.Length);

			output.Flush();
			output.Close();
			br.Close();
		}

        #endregion

		/// <summary>
		/// Archives the file into history
		/// </summary>
		/// <param name="path">file path</param>
		/// <param name="fileId">file name</param>
		/// <returns>the revision number</returns>
        private int ArchiveFile(string filename) { return ArchiveFile(filename, false); }
		private int ArchiveFile(string filename, bool copy)
		{
            string historyDir = GetHistoryDirectory(filename);
            if (!Directory.Exists(historyDir)) Directory.CreateDirectory(historyDir);

            int revision = 1;
            string infoname = filename + FILEINFO_EXTENSTION;
            if (!File.Exists(filename)) return 1;

            try
            {
                ElementInfo info = FileSystemWrapper.GetFileSummary(filename, RepositoryLocation);
                if (info != null) revision = info.Revision;
            }
            catch (Exception) {}
            string postfix = "." + revision + "." + DateTime.Now.Ticks.ToString();
            string dest = Path.Combine(historyDir, Path.GetFileName(filename) + postfix);

            if (copy) File.Copy(filename, dest);
            else File.Move(filename, dest);
            if (File.Exists(infoname))
                File.Move(infoname, Path.Combine(historyDir, Path.GetFileName(infoname) + postfix));

            return revision;
        }

        internal string GetHistoryDirectory(string directoryId)
		{
			string regex = string.Format(@"({0}\\)(.*?)\\(.*)", Path.Combine(RepositoryLocation, "root").Replace("\\", "\\\\"));
			string replace = @"$1_history\$2\$3";

			string historyFile = Regex.Replace(directoryId.Replace('/', '\\'), regex, replace);

			return historyFile;
		}

		private string GetElementPath(string directoryId,string elementId,ElementType type)
		{
			directoryId = Regex.Replace(directoryId.Replace("*", "\\"), @"^\\", "");
			elementId = Regex.Replace(elementId, @"^\*", "").Replace("*", "\\");
            bool inHistory = Regex.IsMatch(directoryId, "_history");
            string dir = "root\\" + (!inHistory ? type == ElementType.image ? ElementType.page.ToString() : type.ToString() : "");

			string filename = Path.Combine(Path.Combine(RepositoryLocation, dir), Path.Combine(directoryId, elementId));

			return filename;
		}

		private void MapChildren(StringBuilder str, string path, string parentId) 
		{ 
			string[] files = Directory.GetFiles(path);  
			
			foreach (string file in files) 
			{
                if(Path.GetExtension(file).ToLower() == FILEINFO_EXTENSTION)
                    continue;

				string id = Path.GetFileName(file);
				ElementInfo info = FileSystemWrapper.GetFileSummary(file, RepositoryLocation);
				str.AppendFormat("<node id=\"{0}\" type=\"{1}\" title=\"{0}\" lockedBy=\"{2}\" />",
					id, info == null ? "image" : "file", info == null ? "" : info.LockedBy);
			}  
			
			string[] dirs = Directory.GetDirectories(path);  
			
			//continue to map sub directories. 
			foreach (string dir in dirs) 
			{ 
				string dirName = Path.GetFileName(dir);  
				
				if (dirName.ToLower() == ".svn" || dirName.ToLower() == "_svn") 
					continue;
				
				string dirId = parentId + "*" + dirName;  
				
				str.Append("<node id=\"" + dirId + "\" type=\"dir\" name=\"" + dirName + "\">"); 
				
				MapChildren(str, dir, dirId); str.Append("</node>"); 
			}

			if (dirs.Length + files.Length == 0)
				str.Append("<node id=\"_no_items_found_\" />");
		}

		#region IPersistanceProvider Members

        public string GenerateRepository(string user, bool isLicensedToKill)
		{
            long lockId;
            lock (this)
	        {
                XmlDocument g = GetGenerationLock();
                if (g != null) throw new Exception(GetGenerationStatus(g, "Repository generation was already initiated by ")+(isLicensedToKill?"\n\nWould you like to terminate it?":""));
                lockId = AcquireGenerationLock(user, "estimating amount of work");
	        }
            UpdateGenerationLock(lockId, CountItemsInDirectory("*")+1, "in process");

            List<string> warnings = new List<string>();
			GenerateDirectory("*", lockId, warnings);

            KillGenerationLock();

            return string.Join("\n", warnings.ToArray());
		}

        public string GenerateRepositoryStatus()
        {
            XmlDocument g = GetGenerationLock();
            return g == null? null : GetGenerationStatus(g, "Repository generation initiated by ");
        }

        private string GetGenerationStatus(XmlDocument g, string prefix)
        {
            string start = DateTime.ParseExact(_(g, "Start"), "u", DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None).ToLocalTime().ToString();
            return prefix + _(g, "User") + " on " + start + ", and is now " + _(g, "Status") + " (" + Math.Round(100 * float.Parse(_(g, "Finished")) / float.Parse(_(g, "Total"))) + "% complete)";
        }

        private void GenerateDirectory(string path, long lockId, List<string> warnings)
        {
            string folder = GetElementPath(path, "", ElementType.page);

            string[] files = Directory.GetFiles(folder);

            foreach (string file in files)
            {
                if (Path.GetExtension(file).ToLower() == FILEINFO_EXTENSTION) continue;
                UpdateGenerationLock(lockId, path, file);
                Thread.Sleep(10);

                string id = Path.GetFileName(file);
                string warning = GeneratePage(id, path);
                if (warning != null) warnings.Add(warning);
            }

            string[] dirs = Directory.GetDirectories(folder);

            //continue to map sub directories. 
            foreach (string dir in dirs)
            {
                if (dir.ToLower() == ".svn" || dir.ToLower() == "_svn") continue;
                UpdateGenerationLock(lockId, path, dir + "/");

                path = path[path.Length - 1] != '*' ? path + "*" : path;

                GenerateDirectory(path + Path.GetFileName(dir), lockId, warnings);
            }
        }

        private long AcquireGenerationLock(string user, string status)
        {
            DateTime start = DateTime.Now;
            XmlDocument xml = new XmlDocument();
            xml.LoadXml("<?xml version=\"1.0\"?><Lock></Lock>");
            XmlElement root = xml.DocumentElement;
            root.AppendChild(xml.CreateElement("User")).InnerText = user;
            root.AppendChild(xml.CreateElement("LockId")).InnerText = start.Ticks.ToString();
            root.AppendChild(xml.CreateElement("Start")).InnerText = start.ToUniversalTime().ToString("u", DateTimeFormatInfo.InvariantInfo);
            root.AppendChild(xml.CreateElement("Status")).InnerText = status;
            root.AppendChild(xml.CreateElement("Finished")).InnerText = "0";
            root.AppendChild(xml.CreateElement("Total")).InnerText = "1";
            root.AppendChild(xml.CreateElement("Path")).InnerText = "";
            root.AppendChild(xml.CreateElement("File")).InnerText = "";

            string path = GenerationLockFile();
            using (XmlWriter writer = new XmlTextWriter(path, null)) xml.Save(writer);

            return start.Ticks;
        }

        private XmlDocument GetGenerationLock()
        {
            XmlDocument xml = new XmlDocument();
            string path = GenerationLockFile();
            try
            {
                using (XmlReader reader = XmlReader.Create(path)) xml.Load(reader);
            }
            catch (Exception) { return null; }
            return xml;
        }

        private void UpdateGenerationLock(long lockId, int total, string status)
        {
            UpdateGenerationLock(lockId, null, null, total, status);
        }

        private void UpdateGenerationLock(long lockId, string path, string file)
        {
            UpdateGenerationLock(lockId, path, file, -1, null);
        }

        private void UpdateGenerationLock(long lockId, string path, string file, int total, string status)
        {
            XmlDocument g = GetGenerationLock();
            if (g == null) throw new Exception("Generation was terminated with partial completion");
            if (long.Parse(_(g, "LockId")) != lockId) throw new Exception(GetGenerationStatus(g, "Generation was terminated and reinitiated by "));
            _(g, "Path", path);
            _(g, "File", file);
            if (total != -1) _(g, "Total", total);
            if (status != null) _(g, "Status", status);
            _(g, "Finished", int.Parse(_(g, "Finished")) + 1);

            string filePath = GenerationLockFile();
            using (XmlWriter writer = new XmlTextWriter(filePath, null)) g.Save(writer);
        }

        public void KillGenerationLock()
        {
            string path = GenerationLockFile();
            File.Delete(path);
        }

        private string GenerationLockFile()
        {
            return Path.Combine(RepositoryLocation, GENERATE_LOCK);
        }

        private string _(XmlDocument xml, string tagName)
        {
            XmlNodeList list = xml.GetElementsByTagName(tagName);
            return list.Count == 0 ? null : list[0].InnerText;
        }

        private string _(XmlDocument xml, string tagName, Object value)
        {
            XmlNodeList list = xml.GetElementsByTagName(tagName);
            if (list.Count != 0) list[0].InnerText = value != null? value.ToString() : "";
            return list.Count == 0 ? null : list[0].InnerText;
        }

        private int CountItemsInDirectory(string path)
        {
            string folder = GetElementPath(path, "", ElementType.page);
            int count = 1 + Directory.GetFiles(folder).Length;

            path = path[path.Length - 1] != '*' ? path + "*" : path;
            string[] dirs = Directory.GetDirectories(folder);
            foreach (string dir in dirs) if (dir.ToLower() != ".svn" && dir.ToLower() != "_svn") count += CountItemsInDirectory(path + Path.GetFileName(dir));

            return count;
        }

        #endregion
	}
}
